/***************************************************************************
 * Copyright 2012-2013 TXT e-solutions SpA
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * This work was performed within the IoT_at_Work Project
 * and partially funded by the European Commission's
 * 7th Framework Programme under the contract ICT-257367.
 *
 * Authors:
 *      Cristoforo Seccia
 *
 * Contributors:
 *        Domenico Rotondi (TXT e-solutions SpA)
 **************************************************************************/
package it.txt.ens.schema.request.factory;

import it.txt.access.capability.commons.identifier.IdentifierGenerator;
import it.txt.access.capability.commons.schema.factory.CapabilitySchemasJaxbFaxtory;
import it.txt.access.capability.commons.schema.validation.CapabilitySchemaValidationHandlerException;
import it.txt.access.capability.commons.utils.CalendarUtils;
import it.txt.access.capability.commons.utils.DOMUtils;
import it.txt.access.capability.schema.AccessRightsCapabilityType;
import it.txt.ens.schema.ObjectFactory;
import it.txt.ens.schema.RequestType;
import it.txt.ens.schema.SubjectType;
import it.txt.ens.schema.factory.ENSRequestFactoryException;
import it.txt.ens.schema.factory.NamespaceMapper;
import it.txt.ens.schema.factory.SchemasLoader;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Locale;
import java.util.ResourceBundle;

import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;

import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * Implements the interface {@link ENSAuthorisationRequestFactory}.
 * @author Cristoforo Seccia (TXT e-solutions SpA)
 */
public class ENSAuthorisationRequestFactoryImpl implements ENSAuthorisationRequestFactory {
    //Removed because of an error in the XML Signature handling
//    private static CapabilitySchemaFactory capabilitySchemaFactory;
//    private static final Logger LOGGER = Logger.getLogger(
//            ENSAuthorisationRequestFactoryImpl.class.getName());
    private static final ResourceBundle MESSAGES = ResourceBundle.getBundle("log-messages/" +
            ENSAuthorisationRequestFactoryImpl.class.getSimpleName(),Locale.ROOT);
    private final CapabilitySchemasJaxbFaxtory wrapper;
    private static ENSAuthorisationRequestFactory factory;
    private final ObjectFactory ensRequestFactory;
    private final String capabilitySchemasRootPath;
    
    private ENSAuthorisationRequestFactoryImpl(String capabilitySchemasRootPath) throws ENSRequestFactoryException{
        try {
//            capabilitySchemaFactory = CapabilitySchemaFactory.getInstance(capabilitySchemasRootPath);
            SchemasLoader loader = new SchemasLoader();
            wrapper = new CapabilitySchemasJaxbFaxtory(
                    loader.loadENSServiceCapabilityEntities(capabilitySchemasRootPath),
                    new NamespaceMapper(), RequestType.class,
                    //added this new class because of an error in the XML Signature handling
                    AccessRightsCapabilityType.class
                    );
            ensRequestFactory = new ObjectFactory();
            this.capabilitySchemasRootPath = capabilitySchemasRootPath;
        } catch (JAXBException ex) {
            throw new ENSRequestFactoryException(ex.getMessage(), ex);
//        } catch (CapabilitySchemaFactoryException ex) {
//            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        } catch (SAXException ex) {
            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        }
    }
    
    public synchronized static ENSAuthorisationRequestFactory getInstance(String capabilitySchemasRootPath) throws ENSRequestFactoryException {
        if(factory == null)
            factory = new ENSAuthorisationRequestFactoryImpl(capabilitySchemasRootPath);
        else if (!factory.getSchemasRootPath().equals(capabilitySchemasRootPath))
            factory = new ENSAuthorisationRequestFactoryImpl(capabilitySchemasRootPath);
        return factory;
    }

    /**
     * Warning! Use this method only if the CapabilitySchemaFactory has been
     * initialized with one of the parameterized 'getInstance(...)' method.
     */
    public synchronized static ENSAuthorisationRequestFactory getInstance() throws ENSRequestFactoryException {
        if(factory == null){
            throw new ENSRequestFactoryException("ENSServiceRequestFactory has not been initialized");
        }
        return factory;
    }

    /* (non-Javadoc)
     * @see it.txt.ens.schema.request.factory.ENSAuthorisationRequestFactory#getENSServiceRequestFromSource(javax.xml.transform.Source)
     */
    @Override
    public synchronized RequestType unmarshal(Source source) throws ENSRequestFactoryException {
        try {
            return (RequestType) wrapper.getUnmarshaller().unmarshal(source);
        } catch (JAXBException ex) {
            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        }
    }

    /* (non-Javadoc)
     * @see it.txt.ens.schema.request.factory.ENSAuthorisationRequestFactory#getENSServiceRequestFromInputStream(java.io.InputStream)
     */
    @Override
    public synchronized RequestType parseInputStream(InputStream inputStream) throws ENSRequestFactoryException {
        try {
            return (RequestType) wrapper.getUnmarshaller().unmarshal(inputStream);
        } catch (JAXBException ex) {
            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        }
    }

    /* (non-Javadoc)
     * @see it.txt.ens.schema.request.factory.ENSAuthorisationRequestFactory#getENSServiceRequestToDocument(it.txt.ens.schema.RequestType)
     */
    @Override
    public synchronized Document marshal(RequestType request) throws ENSRequestFactoryException {
        return getENSServiceRequestToDocument(request, Boolean.FALSE);
    }

    /* (non-Javadoc)
     * @see it.txt.ens.schema.request.factory.ENSAuthorisationRequestFactory#getFragmentENSServiceRequestToDocument(it.txt.ens.schema.RequestType)
     */
    @Override
    public synchronized Document marshalIntoFragment(RequestType request) throws ENSRequestFactoryException {
        return getENSServiceRequestToDocument(request, Boolean.TRUE);
    }

    private synchronized Document getENSServiceRequestToDocument(RequestType request, boolean fragment) throws ENSRequestFactoryException {
        try {
            Document requestDocument = DOMUtils.newDocument();
            wrapper.getMarshaller().setProperty(Marshaller.JAXB_FRAGMENT, fragment);
            wrapper.getMarshaller().marshal(request, requestDocument);
            return requestDocument;
        } catch (ParserConfigurationException ex) {
            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        } catch (JAXBException ex) {
            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        }
    }

    private synchronized RequestType createENSServiceBasicRequest(URI resourceID,
            String subjectID, String operation, Date sessionDeadline)
            throws ENSRequestFactoryException {
        RequestType requestType = ensRequestFactory.createRequestType();
        requestType.setVersion(String.valueOf(ENSSERVICE_REQUEST_VERSION));
        //Set the request istant
        Date now = new Date();
        XMLGregorianCalendar issueIstant = null;
        try {
            issueIstant = CalendarUtils.newXmlGregorianCalendarFromDateAndTime(now, now);
        } catch (DatatypeConfigurationException ex) {
            throw new ENSRequestFactoryException("Error while creating TimeStamp.", ex);
        }
        XMLGregorianCalendar issueIstantUTC = issueIstant.normalize();
        requestType.setTimeStamp(issueIstantUTC);
        //Set the Request ID
        try {
            requestType.setRequestID(IdentifierGenerator.generateIdentifier());
        } catch (NoSuchAlgorithmException ex) {
            throw new ENSRequestFactoryException("Error while generating identifier.", ex);
        }
        //Set the resource id
        requestType.setResourceID(resourceID.toString());
        //Set the subject
        SubjectType subjectType = ensRequestFactory.createSubjectType();
        subjectType.setSubjectID(subjectID);
        requestType.setSubject(subjectType);
        //Set the session dead line
        XMLGregorianCalendar sessionXmlgc = null;
        try {
            sessionXmlgc = CalendarUtils.newXmlGregorianCalendarFromDateAndTime(
                    sessionDeadline, sessionDeadline);
        } catch (DatatypeConfigurationException ex) {
            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        }
        XMLGregorianCalendar sessionXmlgcUTC = sessionXmlgc.normalize();
        requestType.setSessionDeadline(sessionXmlgcUTC);
        //Set the operation
        requestType.setOperation(operation);
        return requestType;
    }

    /* (non-Javadoc)
     * @see it.txt.ens.schema.request.factory.ENSAuthorisationRequestFactory#createENSServiceRequest(java.net.URI, java.lang.String, java.lang.String, java.util.Date, java.lang.String)
     */
    @Override
    public synchronized RequestType create(URI resourceID,
            String subjectID, String operation, Date sessiondeadLine, String capabilityHash)
            throws ENSRequestFactoryException {
        RequestType requestType = createENSServiceBasicRequest(resourceID, subjectID, operation,
                sessiondeadLine);
        //Set the capability
        RequestType.AccessRightsCapability accessRightsCapability =
                ensRequestFactory.createRequestTypeAccessRightsCapability();
        accessRightsCapability.setCapabilityHash(capabilityHash);
        requestType.setAccessRightsCapability(accessRightsCapability);
        return requestType;
    }

    /* (non-Javadoc)
     * @see it.txt.ens.schema.request.factory.ENSAuthorisationRequestFactory#createENSServiceRequest(java.net.URI, java.lang.String, java.lang.String, java.util.Date, it.txt.access.capability.schema.AccessRightsCapabilityType)
     */
    @Override
    public synchronized RequestType create(URI resourceID,
            String subjectID, String operation, Date sessionDeadline,
            AccessRightsCapabilityType capability)
            throws ENSRequestFactoryException {
        RequestType requestType = createENSServiceBasicRequest(resourceID, subjectID, 
                operation, sessionDeadline);
        //Set the capability
        RequestType.AccessRightsCapability accessRightsCapability =
                ensRequestFactory.createRequestTypeAccessRightsCapability();
        accessRightsCapability.setCapability(capability);
        requestType.setAccessRightsCapability(accessRightsCapability);
        return requestType;
    }

    /* (non-Javadoc)
     * @see it.txt.ens.schema.request.factory.ENSAuthorisationRequestFactory#createENSServiceRequest(java.net.URI, java.lang.String, java.lang.String, java.util.Date, java.io.InputStream)
     */
    @Override
    public synchronized RequestType create(URI resourceID,
            String subjectID, String operation, Date sessionDeadline,
            InputStream capabilityStream)
            throws ENSRequestFactoryException {

        RequestType requestType = createENSServiceBasicRequest(resourceID, subjectID, 
                operation, sessionDeadline);
        
        //Set the capability
        RequestType.AccessRightsCapability accessRightsCapability =
                ensRequestFactory.createRequestTypeAccessRightsCapability();
        try {
            Document capabilityDoc = DOMUtils.createDocumentFromInputStream(capabilityStream);
            wrapper.getCapabilitySchemaValidationHandler().invalidateValidationEvents();
            AccessRightsCapabilityType capabilityType = (AccessRightsCapabilityType) 
                    wrapper.getUnmarshaller().unmarshal(new DOMSource(capabilityDoc));
            if (!wrapper.getCapabilitySchemaValidationHandler().getValidationEvents().isEmpty()) {
                throw new CapabilitySchemaValidationHandlerException(
                        wrapper.getCapabilitySchemaValidationHandler().getValidationEvents(),
                        "Capability Validation Schema Errors.");
            }
//            AccessRightsCapabilityType capabilityType = 
//                    capabilitySchemaFactory.createAccessRights(
//                    new DOMSource(capabilityDoc));
            accessRightsCapability.setCapability(capabilityType);
//        } catch (CapabilitySchemaFactoryException ex) {
//            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        } catch (CapabilitySchemaValidationHandlerException ex) {
            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        } catch (ParserConfigurationException ex) {
            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        } catch (SAXException ex) {
            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        } catch (IOException ex) {
            throw new ENSRequestFactoryException(ex.getMessage(), ex);
        } catch (JAXBException e) {
            throw new ENSRequestFactoryException(MESSAGES.getString("capabilityParsingError"), e);
        }
        requestType.setAccessRightsCapability(accessRightsCapability);
        return requestType;
    }

    /* (non-Javadoc)
     * @see it.txt.ens.schema.request.factory.ENSAuthorisationRequestFactory#getSchemasRootPath()
     */
    @Override
    public String getSchemasRootPath() {
        return capabilitySchemasRootPath;
    }
}
